package org.vossnet.sfig;

import static java.lang.String.format;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import org.apache.log4j.Logger;


interface BeanDefSrvcs {
    public abstract void setObjectProperty(Object o, String propertyName, Object value);

    /**
     * Method that uses Java reflection to instantiate a class object instance.
     * Looks to use a constructor that matches the input list of arguments, if
     * any. If the argument list is empty (or a null reference), then a default
     * constructor is used to create the object instance.
     * 
     * @param name class type of object to be created
     * @param ctorArgs list of constructor arguments
     * @param rtnType Class of the returned new object instance. The Class.cast()
     * method will be invoked on the returned object.
     * @return new object instance
     */
    public abstract <T> T newInstance(String name, List<Object> ctorArgs, Class<T> rtnType);
}


abstract class BeanDefServices implements BeanDefSrvcs {
    protected final Logger log;
    protected BeanDefServices(Logger logger) { this.log = logger; }

    protected abstract Class<?> getClass(Object o);
    protected abstract void setProperty(String propName, Method m, Object o, Class<?> t, Object value)
        throws IllegalAccessException, InvocationTargetException;
    protected abstract void setField(Field f, Object o, Object value) throws IllegalAccessException, NoSuchFieldException;

    /* (non-Javadoc)
     * @see org.vossnet.sfig.BeanDefSrvcs#setObjectProperty(java.lang.Object, java.lang.String, java.lang.Object)
     */
    public void setObjectProperty(Object o, String propertyName, Object value) {
        final Class<?> c = this.getClass(o);

        // First see if name is a property ala javabeans
        final String setterName = "set"+Character.toUpperCase(propertyName.charAt(0))+
            propertyName.substring(1,propertyName.length());
        Method m = null;
        final Method[] methods = c.getMethods();
        for (final Method method : methods) {
            if (method.getName().equals(setterName)) {
                m = method;
                break;
            }
        }
        if ( m != null ) {
            try {
                Class<?> t = value.getClass();
                final Class<?>[] pt = m.getParameterTypes();
                if (pt != null && pt.length > 0)
                    t = pt[0];
                this.setProperty(propertyName, m, o, t, value);
            }
            catch (Exception e) {
                log.error(format(   "%s: Can't set property %s using method %s on %s instance:",
                                    e.getClass().getSimpleName(),propertyName,setterName,c.getName()), e);
            }
        }
        else {
            // try for a visible field
            try {
                this.setField(c.getField(propertyName), o, value);
            } catch (IllegalAccessException iae) {
                log.error(format("%s: Can't access property %s using direct field access from %s instance:",
                        iae.getClass().getSimpleName(), propertyName, c.getName()), iae);
            } catch (NoSuchFieldException nsfe) {
                log.error(format("%s: Class %s has no such property/field: %s:",
                        nsfe.getClass().getSimpleName(), c.getName(), propertyName), nsfe);
            }
        }
    }

    protected Object newInstance(Class<?> cls,Class<?> rtnType) throws InstantiationException, IllegalAccessException {
        // no constructor arguments so just
        // create using default constructor
        return cls.newInstance();
    }

    protected Object newInstance(Class<?> cls,Constructor<?> ctor,List<Object> ctorArgs,Class<?> rtnType)
        throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException {
        return ctor.newInstance(ctorArgs.toArray());
    }

    private static String ctorArgsToString(List<Object> ctorArgs) {
        final StringBuilder sbuf = new StringBuilder("( ");
        for (final Object arg : ctorArgs) {
            sbuf.append(arg.toString()).append(", ");
        }
        sbuf.delete(sbuf.lastIndexOf(", "), sbuf.length());
        sbuf.append(" )");
        return sbuf.toString();
    }

    /* (non-Javadoc)
     * @see org.vossnet.sfig.BeanDefSrvcs#newInstance(java.lang.String, java.util.List, java.lang.Class)
     */
    public <T> T newInstance(String name, List<Object> ctorArgs,Class<T> rtnType) {
        Object o = null;
        try {
            final Class<?> cls = Class.forName(name);
            name = cls.getSimpleName();
            if (ctorArgs != null && ctorArgs.size() > 0) {
                log.debug(format("Constructor arguments for class %s:%n\t%s", name, ctorArgsToString(ctorArgs)));
                final Constructor<?>[] ctors = cls.getConstructors();
                if (ctors.length <= 0) // any public constructor?
                    throw new InstantiationException("No public constructor");
                // look for a public constructor that is compatible
                // with the input list of constructor arguments
                String errMsg = "";
                for (final Constructor<?> ctor : ctors) {
                    final Class<?>[] argTypes = ctor.getParameterTypes();
                    if (argTypes.length == ctorArgs.size()) {
                        // check to see if the list of arguments
                        // are type compatible to the constructor
                        boolean match = true;
                        int i = 0;
                        for (final Object arg : ctorArgs) {
                            final Class<?> ctorArgCls = argTypes[i++];
                            final Class<?> arcCls = arg.getClass();
                            if (!ctorArgCls.isAssignableFrom(arcCls)) {
                                errMsg = format(
                                        ":%n\tArgument #%d of type %s not compatible to constructor argument type %s",
                                        i, arcCls.getSimpleName(), ctorArgCls.getSimpleName());
                                match = false;
                                break;
                            }
                        }
                        if (match) {
                            o = this.newInstance(cls, ctor, ctorArgs, rtnType);
                            break;
                        }
                    }
                }
                if (o == null) // any matching constructor?
                    throw new InstantiationException("No compatible constructor found for argument list" + errMsg);
            } else {
                // no constructor arguments so just
                // create using default constructor
                o = this.newInstance(cls, rtnType);
            }
        } catch (Exception e) {
            log.error(format("%s: Can't make instance of class %s due to:%n\t%s",
                            e.getClass().getSimpleName(), name, e.getMessage()), e);
        }
        return rtnType.cast(o);
    }
}
